import { getClientId } from '../utils'
import SocketIO from 'socket.io-client'
import { fromAny, StringValue, Interaction, toAny, Int32Value, Int64Value } from '../proto'
import { Message } from 'protobufjs'
import { getSessionClass } from "../ref";
import { HOST_DEV, HOST_PROD } from '../build'

export class BaseSession {
    private resList: Set<(any) => void>
    private contAutoincrementId: number
    private sessions: Set<BaseSession>
    public onDestroyedListener: () => void
    private id: number
    private parent: BaseSession
    protected render: HTMLElement
    private socketio: SocketIO
    constructor(name: String, render: HTMLElement) {
        this.render = render
        this.resList = new Set()
        this.contAutoincrementId = 0
        this.sessions = new Set()
        const qPlatform = 'platform=Web'
        const qClientId = 'client-id=' + getClientId()
        // 如果是开发环境
        const host = (process.env.NODE_ENV == "development") ? HOST_DEV : HOST_PROD
        this.socketio = SocketIO.connect('http://' + host + ':8001/' + name, {
            query: [qClientId, qPlatform].join('&'),
            path: '',
            transports: ['websocket', 'xhr-polling', 'jsonp-polling'],
        })
        this.socketio.on('connect', () => this.onConnect())
        this.socketio.on('call', (data) => this.onCall(data))
        this.socketio.on('connect_error', () => {
            // alert(err)
        })
        this.socketio.on('disconnect', () => this.destroy())
    }
    /**
     * 销毁会话实例
     */
    destroy () {
        this.onDestroy()
        this.socketio.disconnect()
    }
    /**
     * 发送一个交互动作。
     * @param name 动作名称
     * @param sessionId 会话ID
     * @param futureId future对象ID
     * @param argv 交互动作的额外参数
     */
    sendAction(name: String, sessionId: number = 0, futureId: number = 0, ...argv) {
        if (!Interaction) {
            setTimeout(() => this.sendAction(name, sessionId, futureId, ...argv), 1000)
            return
        }
        const binaryMsg = Interaction.encode({ name, session: sessionId, future: futureId, argv }).finish()
        const offset = binaryMsg.byteOffset
        this.socketio.emit('call', binaryMsg.buffer.slice(offset, offset + binaryMsg.byteLength))
    }
    /**
     * 发送一个交互动作，并等待结果。
     * @param name 动作名称
     * @param sessionId 会话ID
     * @param futureId future对象的ID
     * @param argv 交互动作的额外参数
     * */
    sendActionUntilCallbackResult(name: String, sessionId: number = 0, futureId: number = 0, ...argv) {
        return new Promise((resolve) => {
            const callbackResult = r => resolve(r)
            callbackResult.id = ++this.contAutoincrementId
            this.resList?.add(callbackResult)
            this.sendAction(name, sessionId, futureId, toAny(callbackResult.id, Int32Value), ...(argv || []))
        })
    }
    /**
     * 在服务端调用一个回调函数。
     * @param callback 一个服务端正在监听的回调函数
     * @param argv 传送给函数的参数
     * */
    sendCallback(callback: Message, ...argv) {
        return new Promise((resolve) => {
            const callbackResult = r => resolve(r)
            callbackResult.id = ++this.contAutoincrementId
            this.resList?.add(callbackResult)
            const action = fromAny(callback, Interaction)
            this.sendAction("onCallback", action.session, action.future, toAny(callbackResult.id, Int32Value), ...argv)
        })
    }
    getReference<T extends BaseSession>(session: Message): T {
        const _id = fromAny(session, Int64Value)
        // @ts-ignore
        for (let s of this.sessions)
            if (s.id == _id)
                return s as T
        return this.parent?.getReference<T>(session)
    }
    create(sessionId: number, futureId: number, ...argv) {
        if (!argv || argv.length < 1)
            throw TypeError("Missing param `clsName` from the server calling this `create` method.")
        const clsName = fromAny(argv[0], StringValue)
        const cls = getSessionClass(clsName)
        if (!cls)
            throw TypeError(`The param 'clsName' (value is ${clsName}) is invalid from the server calling this 'create' method.`)
        // @ts-ignore
        const session: BaseSession = new cls(this.render)
        session.parent = this
        session.onDestroyedListener = () => {
            if (this.sessions)
                this.sessions.delete(session)
            session.parent = null
        }
        this.sessions.add(session)
        session.onCreate(sessionId, futureId, ...argv.slice(1, argv.length))
    }
    onCreate(sessionId: number, futureId: number, ...argv) {
        this.sendAction('onCreate', sessionId, futureId, ...argv)
    }
    onDestroy () {
        if (this.resList)
            this.resList.clear()
        this.resList = null
        if (this.sessions) {
            // @ts-ignore
            for (let session of this.sessions)
                session.destroy()
            this.sessions.clear()
        }
        this.sessions = null
        this.sendAction("onDestroy")
        if (this.onDestroyedListener)
            this.onDestroyedListener()
        this.onDestroyedListener = null
    }
    onError(error: Error, sessionId: number = 0, futureId: number = 0) {
        let msg = error?.message
        if (!msg || msg == '')
            msg = "An unknown error."
        this.sendAction("onError", sessionId, futureId, toAny(msg, StringValue))
    }
    onConnect() {}
    onReceive(name: String, sessionId: number, futureId: number, ...argv) {
        switch (name) {
            case 'ready':
                this.ready(...argv)
                break
            case 'create':
                this.create(sessionId, futureId, ...argv)
                break
            case 'destroy':
                this.destroy()
                break
            case 'callbackResult':
                this.callbackResult(...argv)
                break
        }
    }
    private onCall(data: ArrayBuffer) {
        const msg = Interaction.decode(new Uint8Array(data))
        try {
            // @ts-ignore
            this.onReceive(msg.name, msg.session, msg.future, ...msg.argv)
        } catch (error) {
            console.log(error.stack)
            // @ts-ignore
            this.onError(error, msg.session, msg.future)
        }
    }
    private ready(...argv) {
        this.id = fromAny(argv[0], Int64Value)
    }
    private callbackResult(...argv) {
        const cont = fromAny(argv[0], Int32Value)
        // @ts-ignore
        const resolve = [...this.resList].find(r => r.id == cont)
        if (!resolve) return
        this.resList.delete(resolve)
        resolve(argv[1])
    }
}